iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Mobile Development

30 天輕鬆學會 Flutter 測試系列 第 11

Day 11 我們會重構程式碼,那測試呢?

  • 分享至 

  • xImage
  •  

我們連續講了好幾天的測試,講了如何寫單元測試,如何隔離依賴,又如何測試錯誤,既然我們寫了這麼多測試,我們就得花時間維護。當測試回報錯誤的時候,我們回頭看測試,發現測試寫得一團亂,是不是就開始不太想處理了呢?加上問題可能來得又急又快,可能就直接放棄治療了。所以我們得在平常測試時,就把測試整理乾淨,當問題來臨時,才不會手忙腳亂。

乾淨的測試

如果說重構程式碼,我們有許多設計原則可以遵循,例如:高內聚/低耦合、SOLID 原則 …等等,在正式程式碼中,我們十分在意程式碼是否耦合,是否容易維護,同時也要確保程式碼意圖是否明確。而在測試程式碼,我們首要在意的則是可讀性,畢竟當測試完成之後,我們下次看的時候可能就是出問題的時候,測試越容易理解,我們就能越快發現問題在哪邊。讓我們比兩段測試程式碼看看。

test("purchase product success", () async {
  var mockProductRepository = MockProductRepository();
  var mockWalletRepository = MockWalletRepository();

  when(mockWalletRepository.get()).thenAnswer((_) async => Wallet(100));

  var purchaseProductService = PurchaseProductService(
    mockProductRepository,
    mockWalletRepository,
  );

  var coupon = Coupon(discount: 0.5, expiredAt: DateTime.now().add(const Duration(days: 10)));
  const product = Product(100);
  await purchaseProductService.execute(product, coupon);

  verify(mockProductRepository.purchase(product, coupon)).called(1);
});

測試中寫了很多程式碼,包含建立測試替身,設定參數與執行,最後驗證等等。想像一下,三個月後回來看這段測試,可能就必須從頭開始理解。

test("purchase product success", () async {
  given_wallet(Wallet(100));

  const product = Product(100);
  var coupon = Coupon(discount: 0.5, expiredAt: after(days: 10));

  await when_purchase(product, coupon);

  then_repository_should_be_call(product, coupon);
});

比較這兩段程式碼後,可以發現後者比較明確呈現了測試的行為,把不重要的細節隱藏在方法中,並用方法名稱來呈現目的。

在書籍或文章的架構中,作者會用標題概述一下本段內容,寫程式或者寫測試也是同樣的道理,方法名稱就是標題,好的標題能讓人一看就知道方法目的是什麼。當我們了解的重構測試的重要性之後,下一個問題就是,該如何重構測試。

Setup 與 Teardown

大多數測試框架都會提供一些 Callback,讓我們可以在測試的前後時間點處理一些雜事。Flutter 提供幾個方法讓開發人員使用:

  • setUpAll:當所有測試開始之前被呼叫
  • setUp:每一個測試開始之前被呼叫
  • tearDown:每一個測試結束之後被呼叫
  • tearDownAll:當所有測試結束之後被呼叫
main() {
	setUp(() {
	  mockProductRepository = MockProductRepository();
	  mockWalletRepository = MockWalletRepository();
	  purchaseProductService = PurchaseProductService(mockProductRepository, mockWalletRepository);
	});
}

在上面的例子中,我們就使用 setUp 來集中處理 Mock 與 SUT 的建立,把一些共用但是不重要的東西放到 Setup 與 Teardown 中,只讓重要的測試流程出現在測試案例中。

Group 相關測試

當我們在測試某個方法時,可能會針對這個方法設計許多測試案例,而這些相關的測試案例,可以用 group 把他們放在一起,讓開發人員可以從測試中直觀地看出這組測試可能是在測試同一個方法的不同行為。

main() {
  group("purchase product", (){
    test("purchase success", () async {
      // ...
    });

    test("purchase fail", () {
      // ...
    });
  });
}

Given-When-Then 風格

在實務上,我們可以借用 Given-When-Then 風格:Given、When、Then,這個語法通常用在 BDD 中,但是我們也可以把它運用在單元測試裡。Given 對應 3A 原則的 Arrange,而 When 是 Act,而 Then 則是 Assert,測試的細節封裝到方法中,用方法名稱解釋測試每一步的行為,閱讀時就能更快理解這一步驟對目的。

given_wallet(Wallet(100));

await when_purchase(product, coupon);

then_repository_should_be_call(product, coupon);

在上面的例子中,我們運用抽取方法把三個階段的程式碼整理一下,讓測試能直觀的呈現 3A 原則,每一行程式碼都在同一個抽象層次,讓測試更容易理解。

用底線區分

除了使用 Given-When-Then 風格之外,我們還在方法名稱上更進一步,若是不習慣看英文的觀眾,用駝峰式命名法來命名的話,可能會不太好閱讀,此時我們可以透過蛇型命名法,在每一個單字之間加上底線,讓單字之間更清楚,幫助我們更快閱讀英文。

given_wallet(...)

when_purchase(...)

then_repository_should_be_call(...)

這個技巧也是 91 在課堂中分享過的技巧,有興趣的朋友可以參考 https://tdd.best/author/joeychen/

值得注意的是,如果我們在測試中的方法使用蛇型命名法,可能會被 lint 警告,因為 Dart 預設的方法命名規則是小駝峰。此時我們可以在測試檔案中忽略這個 lint,讓測試中檔案不會到處都是警告。

// ignore_for_file: non_constant_identifier_names

以上方法並非強制,更多的是取決於團隊習慣,畢竟閱讀這些測試的是團隊,維護這些測試的也是團隊,如何能增進團隊對於測試的理解速度,也就依賴於團隊自身的習慣。

小結

雖然我們提了許多重構測試的方法,但其實重構測試倒是沒有一定要怎麼做才對,而是以團隊習慣為主,畢竟大多時候,閱讀測試的人就是團隊自己,任何有助於團隊閱讀測試或節省寫測試時間的做法,都是十分值得嘗試的。


上一篇
Day 10 測試每執行五秒,開發者就少了五秒
下一篇
Day 12 單元測試回顧
系列文
30 天輕鬆學會 Flutter 測試30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言